import math
from collections import defaultdict
from typing import Union

number = 'number'


def item_similarity(train):
    # c[i][number]表示使用物品i的用户数量
    # c[i][j]表示同时交互物品i和j的用户数
    c = defaultdict(lambda: defaultdict(int))
    for user, items in train.items():
        for i in items:
            # 统计每个物品被交互的总次数
            c[i][number] += 1
            # 统计物品i与其他物品的共现次数
            for j in items:
                if i == j:
                    continue
                c[i][j] += 1

    # 计算最终的相似度矩阵 w
    w = defaultdict(dict)
    for i, related_items in c.items():
        for j, cij in related_items.items():
            if j == number: continue
            # 余弦相似度公式
            similarity = cij / math.sqrt(c[i][number] * c[j][number])
            w[i][j] = similarity

    return w


def recommend(interacted_items: Union[set, dict], w, k):
    """
    :param interacted_items: 指定用户交互过的物品
    :param w: 物品的相似度
    :param k: 取最相似的k个物品
    :return:
    """
    if isinstance(interacted_items, set):  # 如果只有物品，没有评分，那么将评分统一设置为1
        interacted_items = {k: 1 for k in interacted_items}
    rank = defaultdict(float)
    # 用户交互过的物品，和用户对这个物品的评分
    for item, score in interacted_items.items():
        # 物品的相似度信息，得到related_item和item的相似度similarity，按照相似度的值从大到小排序，取k个值
        for related_item, similarity in sorted(w[item].items(), key=lambda x: x[1], reverse=True)[:k]:
            # 如果这个物品已经被用户交互过了，跳过
            if related_item in interacted_items:
                continue
            # 计算相关的物品的相似度评分
            rank[related_item] += score * similarity
    return rank

def precision_recall(test_data, train_data, n, recommend_func):
    """
    计算推荐系统在测试集上的准确率和召回率

    :param test_data: dict，用户 -> 测试集中真实交互物品列表
    :param train_data: dict，用户 -> 训练集中交互物品列表（用于生成推荐）
    :param n: int，每个用户推荐的物品数量
    :param recommend_func: function(user, n, train_data)，返回推荐物品列表
    :return: [recall, precision]
    """
    hit = 0  # 交集
    total_actual = 0  # 所有用户的真实物品总数
    total_recommend = 0  # 所有推荐物品总数

    for user, actual_items in test_data.items():
        # 计算推荐物品
        recommended_items = recommend_func(user, n, train_data)
        # 计算交集
        hit += len(set(recommended_items) & set(actual_items))
        # 真实物品数
        total_actual += len(actual_items)
        # 推荐物品数
        total_recommend += len(recommended_items)

    recall = hit / total_actual if total_actual else 0
    precision = hit / total_recommend if total_recommend else 0
    return [recall, precision]

def main():
    users = {
        'A': {'a', 'b', 'd'},
        'B': {'a', 'c'},
        'C': {'b', 'e', 'a'},
        'D': {'c', 'd', 'e'}
    }
    w = item_similarity(users)
    for k, v in w.items():
        print(f'{k}: {dict(sorted(v.items(), key=lambda item: item[1], reverse=True))}')

    rank = recommend(users['B'], w, 3)
    for k, v in sorted(rank.items(), key=lambda item: item[1], reverse=True):
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()